diff --git a/.github/workflows/cross-compile.yml b/.github/workflows/cross-compile.yml new file mode 100644 index 0000000..d919643 --- /dev/null +++ b/.github/workflows/cross-compile.yml @@ -0,0 +1,53 @@ +# inspired by https://github.com/Timmmm/rust_cross_compile_demo/blob/master/.github/workflows/build.yaml +name: Cross-Compile + +on: [push] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + if: github.actor == 'topheman' && contains(github.event.head_commit.message, 'cross-compile-all') + runs-on: ubuntu-latest + + steps: + + - name: Set up MacOS Cross Compiler + uses: Timmmm/setup-osxcross@v2 + with: + osx-version: "12.3" + + - name: Install Rustup targets + run: rustup target add x86_64-unknown-linux-gnu x86_64-apple-darwin aarch64-apple-darwin + + - name: Check out source code + uses: actions/checkout@v3 + + - name: Check + run: cargo check + + - name: Build + run: cargo build --release --target x86_64-unknown-linux-gnu --target x86_64-apple-darwin --target aarch64-apple-darwin + + - name: Compress + run: | + (cd target/x86_64-unknown-linux-gnu/release && tar -cvf snakepipe-x86_64-unknown-linux-gnu.tar.gz snakepipe && mv snakepipe-x86_64-unknown-linux-gnu.tar.gz ../../..) + (cd target/x86_64-apple-darwin/release && tar -cvf snakepipe-x86_64-apple-darwin.tar.gz snakepipe && mv snakepipe-x86_64-apple-darwin.tar.gz ../../..) + (cd target/aarch64-apple-darwin/release && tar -cvf snakepipe-aarch64-apple-darwin.tar.gz snakepipe && mv snakepipe-aarch64-apple-darwin.tar.gz ../../..) + + - name: Calculate sha256 + run: | + shasum -a 256 snakepipe-x86_64-unknown-linux-gnu.tar.gz >> sha256.txt + shasum -a 256 snakepipe-x86_64-apple-darwin.tar.gz >> sha256.txt + shasum -a 256 snakepipe-aarch64-apple-darwin.tar.gz >> sha256.txt + + - name: Upload Binaries + uses: actions/upload-artifact@v3 + with: + name: binaries + path: | + snakepipe-x86_64-unknown-linux-gnu.tar.gz + snakepipe-x86_64-apple-darwin.tar.gz + snakepipe-aarch64-apple-darwin.tar.gz + sha256.txt diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..a13f31b --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,18 @@ +name: CI + +on: [push, pull_request] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose diff --git a/.gitignore b/.gitignore index ea8c4bf..cc2647d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ +# rust /target +render.out + +# nodejs +node_modules +dist diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..ea1a12b --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "rust-lang.rust-analyzer", + "skellock.just" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ad92582 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": true +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8b9c48e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing + +This is a rust project, but you can contribute as a rust developer as much as a JavaScript developer. + +## Rust + +You will find your way in the the source code: [`./src`](./src/). + +To build the project: + +```sh +cargo build +``` + +To run: + +```sh +./target/debug/snakepipe # will show the help - you can use it is explained in the README +``` + +## JavaScript + +The [`snakepipe render-browser`](./README.md#-you-can-mirror-your-playing-terminal-into-another-one-through-http) command launches a rust http server that serves some JavaScript code that connects to server-sent events and renders the game inside the browser. + +The source code for the renderers is available at [`static/renderers`](static/renderers). + +**You don't need rust to work on this part**. I made a nodejs development server that acts as the `snakepipe render-browser` command (that way, you also don't have to re-build the rust part each time you modify the frontend). + +### Install + +```sh +npm install +``` + +### Setup + +Build the packages and retrieve a recorded party. + +```sh +npm run build && npm run setup +``` + +### Run + +```sh +npm run dev-server-sse +``` + +Go to [http://localhost:8080](http://localhost:8080). diff --git a/Cargo.lock b/Cargo.lock index 815398f..524cb3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,9 +3,2322 @@ version = 3 [[package]] -name = "cli" +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.4.2", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d223b13fd481fc0d1f83bb12659ae774d9e3601814c68a0bc539731698cca743" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash", + "base64", + "bitflags 2.4.2", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.48", +] + +[[package]] +name = "actix-router" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22475596539443685426b6bdadb926ad0ecaefdfc5fb05e5e3441f15463c511" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb13e7eef0423ea6eab0e59f6c72e7cb46d33691ad56a726b3cd07ddec2c2d4" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a6556ddebb638c2358714d853257ed226ece6023ef9364f23f0c70737ea984" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1f50ebbb30eca122b188319a4398b3f7bb4a8cdf50ecfb73bfc6a3c3ce54f5" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "actix-web-lab" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7675c1a84eec1b179c844cdea8488e3e409d8e4984026e92fa96c87dd86f33c6" +dependencies = [ + "actix-http", + "actix-router", + "actix-service", + "actix-utils", + "actix-web", + "actix-web-lab-derive", + "ahash", + "arc-swap", + "async-trait", + "bytes", + "bytestring", + "csv", + "derive_more", + "futures-core", + "futures-util", + "http", + "impl-more", + "itertools", + "local-channel", + "mediatype", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_html_form", + "serde_json", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "actix-web-lab-derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa0b287c8de4a76b691f29dbb5451e8dd5b79d777eaf87350c9b0cbfdb5e968" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "actix-web-static-files" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf6d1ef6d7a60e084f9e0595e2a5234abda14e76c105ecf8e2d0e8800c41a1f" +dependencies = [ + "actix-web", + "derive_more", + "futures-util", + "static-files", +] + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + +[[package]] +name = "array2d" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b39cb2c1bf5a7c0dd097aa95ab859cf87dab5a4328900f5388942dc1889f74" + +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "bytestring" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "change-detection" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "159fa412eae48a1d94d0b9ecdb85c97ce56eb2a347c62394d3fdbf221adabc1a" +dependencies = [ + "path-matchers", + "path-slash", +] + +[[package]] +name = "clap" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctrlc" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b467862cc8610ca6fc9a1532d7777cee0804e678ab45410897b9396495994a0b" +dependencies = [ + "nix", + "windows-sys 0.52.0", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] + +[[package]] +name = "exitcode" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "h2" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "impl-more" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d" + +[[package]] +name = "indexmap" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "js-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-ip-address" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136ef34e18462b17bf39a7826f8f3bbc223341f8e83822beb8b77db9a3d49696" +dependencies = [ + "libc", + "neli", + "thiserror", + "windows-sys 0.48.0", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "mediatype" +version = "0.19.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8878cd8d1b3c8c8ae4b2ba0a36652b7cf192f618a599a7fbdfa25cffd4ea72dd" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "neli" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1100229e06604150b3becd61a4965d5c70f3be1759544ea7274166f4be41ef43" +dependencies = [ + "byteorder", + "libc", + "log", + "neli-proc-macros", +] + +[[package]] +name = "neli-proc-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168194d373b1e134786274020dae7fc5513d565ea2ebb9bc9ff17ffb69106d4" +dependencies = [ + "either", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "libc", +] + +[[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-conv" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "owo-colors" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" [[package]] -name = "common" +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "path-matchers" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36cd9b72a47679ec193a5f0229d9ab686b7bd45e1fbc59ccf953c9f3d83f7b2b" +dependencies = [ + "glob", +] + +[[package]] +name = "path-slash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498a099351efa4becc6a19c72aa9270598e8fd274ca47052e37455241c88b696" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +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 = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.11.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg", +] + +[[package]] +name = "reqwest-eventsource" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f529a5ff327743addc322af460761dff5b50e0c826b9e6ac44c3195c50bb2026" +dependencies = [ + "eventsource-stream", + "futures-core", + "futures-timer", + "mime", + "nom", + "pin-project-lite", + "reqwest", + "thiserror", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + +[[package]] +name = "serde" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_html_form" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20e1066e1cfa6692a722cf40386a2caec36da5ddc4a2c16df592f0f609677e8c" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_json" +version = "1.0.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "snakepipe" +version = "2.0.0" +dependencies = [ + "actix-web", + "actix-web-lab", + "actix-web-static-files", + "array2d", + "clap", + "crossterm", + "ctrlc", + "exitcode", + "futures-util", + "indexmap", + "local-ip-address", + "owo-colors", + "parking_lot", + "rand", + "reqwest", + "reqwest-eventsource", + "serde", + "serde_json", + "static-files", + "tokio", + "tokio-stream", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "static-files" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64712ea1e3e140010e1d9605872ba205afa2ab5bd38191cc6ebd248ae1f6a06b" +dependencies = [ + "change-detection", + "mime_guess", + "path-slash", +] + +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "thiserror" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" + +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] diff --git a/Cargo.toml b/Cargo.toml index 836d936..96b6038 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,47 @@ -[workspace] -resolver = "2" -members = [ - "crates/cli", - "crates/common" -] +[package] +name = "snakepipe" +version = "2.0.0" +edition = "2021" +repository = "https://github.com/topheman/snake-pipe-rust" +authors = ["Christophe Rosset"] +keywords = ["cli", "game", "snake", "unix", "pipe"] +description = "A snake game based on stdin/stdout following unix philosophy" +license = "MIT" +build = "build.rs" + +[[bin]] +name = "snakepipe" +path = "src/main.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "0.8.5" +clap = { version = "4.0", features = ["derive", "cargo"] } +crossterm = "0.27.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +array2d = "0.3.0" +ctrlc = "3.4.2" +indexmap = "2.2.3" +# Must opt out of compress-zstd, because it will fail cross compilation +# list of actix-web features: https://docs.rs/actix-web/latest/actix_web/#crate-features +actix-web = { version = "4.5.1", default-features = false, features = [ + "macros", + "compress-gzip", +] } +actix-web-static-files = "4.0.1" +static-files = "0.2.3" +futures-util = "0.3.30" +parking_lot = "0.12.1" +tokio = { version = "1.36.0", features = ["full"] } +tokio-stream = "0.1.14" +actix-web-lab = "0.20.2" +reqwest-eventsource = "0.5.0" +reqwest = { version = "0.11.24", features = ["json"] } +owo-colors = "4.0.0" +local-ip-address = "0.6.1" +exitcode = "1.1.2" + +[build-dependencies] +static-files = "0.2.3" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ba01647 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright (C) 2024 Christophe Rosset + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/PUBLISHING.md b/PUBLISHING.md new file mode 100644 index 0000000..60a2cd4 --- /dev/null +++ b/PUBLISHING.md @@ -0,0 +1,44 @@ +# Publishing + +This document is reserved for the publishing part - you don't need it as a consumer or as a contributer, this is for me. + +I share it because, some people might find it useful and others might find ways to improve it. + +## Homebrew + +The homebrew formula for snakepipe is hosted at [topheman/homebrew-tap](https://github.com/topheman/homebrew-tap/blob/main/Formula/snakepipe.rb). + +The binaries are available at [topheman/snake-pipe-rust/releases](https://github.com/topheman/snake-pipe-rust/releases). + +### Before setup + +Add `export HOMEBREW_EDITOR="code -w"` to your `.bashrc`/`.zshrc` if you prefer editing the Formulaes with `vscode`. + +### Setup + +You only have to do it once, providing a url of a compressed binary: + +```sh +brew create --tap topheman/tap https://github.com/topheman/snake-pipe-rust/releases/download/v2.0.0/snakepipe-x86_64-apple-darwin.tar.gz +``` + +You will prompt with a formula, that you can customize. I added the targets for x86_64 and aarch64. + +Close the the editor. + +To test the formula in local: + +```sh +brew audit --strict --new --online snakepipe +``` + +Then, you can access the repository that was created for you and customize it / push it to a remote (in my case to [topheman/homebrew-tap](https://github.com/topheman/homebrew-tap)): + +```sh +cd $(brew --repository topheman/tap) +``` + +Sources: + +- https://publishing-project.rivendellweb.net/creating-and-running-your-own-homebrew-tap/ +- https://github.com/kcctl/homebrew-tap/blob/main/Formula/kcctl.rb diff --git a/README.md b/README.md new file mode 100644 index 0000000..1814eb9 --- /dev/null +++ b/README.md @@ -0,0 +1,266 @@ +# snake-pipe-rust + +[![crates.io](https://img.shields.io/crates/v/snakepipe.svg)](https://crates.io/crates/snakepipe) [![Docs](https://docs.rs/snakepipe/badge.svg)](https://docs.rs/snakepipe/latest/snakepipe/) [![Build](https://github.com/topheman/snake-pipe-rust/actions/workflows/rust.yml/badge.svg?label=build)](https://github.com/topheman/snake-pipe-rust/actions/workflows/rust.yml) + +Not just yet another snake game in the terminal 😉. + +https://github.com/topheman/snake-pipe-rust/assets/985982/76161595-1c3a-4252-9cbd-25e144bf185c + +This one follows the [unix philosophy](https://en.wikipedia.org/wiki/Unix_philosophy) as: + +- `snakepipe gamestate` accepts user inputs, calculates the state of the game and writes it to `stdout` +- `snakepipe render` reads the state of the game from `stdin` and renders it on the terminal +- `snakepipe throttle` reads a pre-recorded game from `stdin` and writes to `stdout` each tick so that `snakepipe render` can pick it up +- `snakepipe render-browser` spawns a server and sends `stdin` via server-sent events to a JavaScript renderer in your browser +- `snakepipe stream-sse` connects to the server spawned by `render-browser` and streams server-sent events back to the terminal +- `snakepipe pipeline ` prints out the most common pipelines (combinations of commands), so that you could directly `pbcopy`/paste them + +That way: + +- you could write your own version of the `gamestate` or `render` command in any programming language and make it work with mine +- it's a great exercise to handle stream serialization/deserialization in rust + +## Motivation + +I've already done [a few rust projects](http://labs.topheman.com) (with WebAssembly or [bevy](https://github.com/topheman/bevy-rust-wasm-experiments)), however, I wanted something that needs to deal directly with: + +- I/O +- parsing +- parallelism +- async programming +- handling piping/stdin/stdout/signaling ... + +## Install + +Any OS - if you have Rust >= 1.75.0 - [How to install Rust (if you don't have it yet)](https://www.rust-lang.org/tools/install) + +```sh +cargo install snakepipe +``` + +On MacOS, with Homebrew: + +```sh +brew install topheman/tap/snakepipe +``` + +Other OS: see [releases](https://github.com/topheman/snake-pipe-rust/releases). + +## Usage + +### 🎮 Play in terminal + +```sh +# basic usage +snakepipe gamestate|snakepipe render + +# change the defaults +snakepipe gamestate --frame-duration 80 --width 70 --height 20 --snakepipe-length 15|snakepipe render + +# call help on any of the commands +snakepipe --help +``` + +### 📼 You can even record and replay using basic piping + +```sh +# record a game into a file using the builtin `tee` command utility +snakepipe gamestate|tee /tmp/snakepipe-output|snakepipe render + +# replay the game you recorded +cat /tmp/snakepipe-output|snakepipe throttle|snakepipe render +``` + +### 📺 You can also mirror your playing terminal into another one + +Open two terminals that will communicate via a file that will be `tail`ed and piped to `snakepipe render` + +```sh +# mirroring terminal +cat /dev/null > /tmp/snakepipe-output && tail -f /tmp/snakepipe-output|snakepipe render +``` + +```sh +# main terminal +snakepipe gamestate|tee /tmp/snakepipe-output|snakepipe render +``` + +### 🖥 You can mirror your playing terminal into a server you can open in a browser + +```sh +snakepipe gamestate|snakepipe render-browser|snakepipe render +``` + +Then open [http://localhost:8080](http://localhost:8080). You'll be able to switch between renderers in your browser as you are playing in your terminal (thanks to server-sent events). + +### 🖼 You can mirror your playing terminal into another one, through http + +Open two terminals: + +```sh +# main terminal: +# - accepts user inputs +# - spawns an http server that streams stdin to server-sent events +# - renders the game to the terminal so you can play +snakepipe gamestate|snakepipe render-browser|snakepipe render +``` + +```sh +# mirroring terminal (not necessary the same device, only need to be on the same network): +# - connects to the http server and streams server-sent events to sdout +# - render the gamestate retrieved from the server +snakepipe stream-sse|snakepipe render +``` + +You could share your game accross your LAN! + +### 😉 And maybe you'll find other ways?... + +## Manual of commands + +
+ snakepipe --help +
A snake game based on stdin/stdout following unix philosophy
+
+Usage: snakepipe \
+
+Commands:
+
+  gamestate       Accepts user inputs (arrow keys to control the snake) and outputs the state of the game to stdout
+  render          Reads gamestate from stdin and renders the game on your terminal
+  throttle        Reads stdin line by line and outputs each line on stdout each `frame_duration` ms (usefull for replaying a file)
+  render-browser  Let's you render the game in your browser at http://localhost:8080 by spawning a server and sending stdin via server-sent events to a JavaScript renderer
+  stream-sse      Connects to the server spawned by `render-browser` and streams server-sent events back to the terminal
+  help            Print this message or the help of the given subcommand(s)
+
+Options:
+  -h, --help     Print help
+  -V, --version  Print version
+  
+
+ +
+ snakepipe gamestate --help +
Accepts user inputs (arrow keys to control the snake) and outputs the state of the game to stdout
+
+Usage: snakepipe gamestate [OPTIONS]
+
+Options:
+      --frame-duration \  in ms [default: 120]
+      --width \                    default 25
+      --height \                  default 25
+      --snake-length \      [default: 2]
+      --fit-terminal
+  
+
+ +
+ snakepipe render --help +
+Reads gamestate from stdin and renders the game on your terminal
+
+Usage: snakepipe render
+  
+
+ +
+ snakepipe throttle --help +
+Reads stdin line by line and outputs each line on stdout each `frame_duration` ms (usefull for replaying a file)
+
+Usage: snakepipe throttle [OPTIONS]
+
+Options:
+      --frame-duration \  in ms [default: 120]
+      --loop-infinite
+  
+
+ +
+ snakepipe render-browser --help +
+Let's you render the game in your browser at http://localhost:8080 by spawning a server and sending stdin via server-sent events to a JavaScript renderer
+
+Usage: snakepipe render-browser [OPTIONS]
+
+Options:
+      --port   [default: 8080]
+  
+
+ +
+ snakepipe stream-sse --help +
+Connects to the server spawned by `render-browser` and streams server-sent events back to the terminal
+
+Usage: snakepipe stream-sse [OPTIONS]
+
+Options:
+      --address \
[default: http://localhost:8080] +
+
+ +
+ snakepipe pipeline --help +
+Prints out some common pipelines, so that you can copy/paste them to execute (you can pipe to `pbcopy`)
+
+Usage: snakepipe pipeline [OPTIONS] [COMMAND]
+
+Commands:
+  play        Play in the terminal
+  record      Record a party in the terminal
+  replay      Replay a party you recorded in the terminal
+  file-play   Play and share a party via a shared file in realtime
+  file-watch  Render the party you are sharing through a file in realtime
+  http-play   Play and share a party through an http server
+  http-watch  Render the party you shared through the http server, in the terminal
+  
+
+ +## Using as a library + +```sh +cargo add snakepipe # add it to your project +``` + +This crate is a cli, but it also exports a lib from where you can import a few utilities, such as `snakepipe::stream::parse_gamestate` - [direct link to docs.rs](https://docs.rs/snakepipe/latest/snakepipe/stream/fn.parse_gamestate.html): + +```rust +use snakepipe::stream::{parse_gamestate, Game}; + +fn main() -> () { + match parse_gamestate() { + Ok(stream) => { + println!( + "Frame duration {}, Snake length {}, Level {}x{}", + stream.options.frame_duration, + stream.options.snake_length, + stream.options.size.width, + stream.options.size.height + ); + for parsed_line in stream.lines { + do_something(parsed_line); + } + } + Err(e) => { + println!("Error occurred while parsing stdin: \"{}\"", e); + } + } +} + +fn do_something(parsed_line: Game) { + println!("Snake head position {:?}", parsed_line.snake.head) +} +``` + +## Contributing + +You can: + +- Make an implementation of the actual `snakepipe render` command for the terminal in an other language than rust +- Make your own JavaScript renderer for the `snakepipe render-browser` command and ask for a PR to integrate it to the project + +An Experimental/Partial nodejs implementation of this crate available at [topheman/snake-pipe-node](https://github.com/topheman/snake-pipe-node). + +More infos in [CONTRIBUTING.md](./CONTRIBUTING.md). diff --git a/RECORDING_NOTES.md b/RECORDING_NOTES.md new file mode 100644 index 0000000..d0d3a96 --- /dev/null +++ b/RECORDING_NOTES.md @@ -0,0 +1,83 @@ +# Recording notes + +Those are the recording notes I use when making the little demo video. + +--- + +Change the `$PS1` + +```sh +export PS1='%{%B%}topheman/snake-pipe-rust%{$reset_color%}% > ' +``` + +--- + +link `snakepipe` to the debug binary in the project + +```sh +SNAKE_PATH="$PWD/target/debug" +export PATH="$SNAKE_PATH:$PATH" +``` + +--- + +cmd+K + +--- + +```sh +cargo install snakepipe +``` + +--- + +```sh +snakepipe +``` + +--- + +```sh +# Run the gamestate command +# which accepts user inputs and passes the game state to stdout + +snakepipe gamestate +``` + +--- + +```sh +# Pipe the output of gamestate command to the render command + +snakepipe gamestate|snakepipe render +``` + +--- + +```sh +# Record a party by saving the output of the gamestate command to a file with the built-in tee utility + +snakepipe gamestate|tee /tmp/snake-output|snakepipe render +``` + +--- + +```sh +# Replay a party by reading the previous file and streaming it to the render command with the throttle command + +cat /tmp/snake-output|snakepipe throttle|snakepipe render +``` + +--- + +```sh +# Thank you +``` + +--- + +Convert `.mov` to `.mp4` + +```sh +ffmpeg -i snake-pipe-rust.mov -vcodec h264 -acodec aac snake-pipe-rust.mp4 +``` diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..b4798ab --- /dev/null +++ b/build.rs @@ -0,0 +1,5 @@ +use ::static_files::resource_dir; + +pub fn main() -> std::io::Result<()> { + resource_dir("./static").build() +} diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml deleted file mode 100644 index 3ae314a..0000000 --- a/crates/cli/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "cli" -version = "0.1.0" -edition = "2021" - -[[bin]] -name = "snake" -path = "src/main.rs" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs deleted file mode 100644 index e7a11a9..0000000 --- a/crates/cli/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml deleted file mode 100644 index b7723d9..0000000 --- a/crates/common/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "common" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs deleted file mode 100644 index 7d12d9a..0000000 --- a/crates/common/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} diff --git a/debug/snakepipe-node/validate-basic.js b/debug/snakepipe-node/validate-basic.js new file mode 100644 index 0000000..e856618 --- /dev/null +++ b/debug/snakepipe-node/validate-basic.js @@ -0,0 +1,39 @@ +/** + * This is a simplified version of the source code for `snakepipe-node validate` + * + * https://github.com/topheman/snake-pipe-node/blob/master/src/validate.ts + * https://github.com/topheman/snake-pipe-node/blob/master/src/common.ts + * + * See https://github.com/topheman/snake-pipe-rust/issues/25 + */ +const readline = require('node:readline'); +const { stdin } = require('node:process'); + +function makeWriteLine(stdout) { + if (stdout.isTTY) { + return function writeLineToTTY(str) { + process.stdout.write(`${str}\n`); + process.stdout.cursorTo(0); + }; + } else { + return function writeLine(str) { + console.log(str); + }; + } +} + +async function main() { + const readStdin = readline.createInterface({ input: stdin }); + const writeLine = makeWriteLine(process.stdout); + + const stdinIterator = readStdin[Symbol.asyncIterator](); + const options = JSON.parse((await stdinIterator.next()).value); + + writeLine(JSON.stringify(options)); + for await (const line of stdinIterator) { + const parsedLine = JSON.parse(line); + writeLine(JSON.stringify(parsedLine)); + } +} + +main(); diff --git a/justfile b/justfile new file mode 100644 index 0000000..8c534c4 --- /dev/null +++ b/justfile @@ -0,0 +1,6 @@ +default: + @echo "Call just --list to see available tasks" +build-x86-apple: + cargo build --release --target x86_64-apple-darwin +build-arm-apple: + cargo build --release --target aarch64-apple-darwin diff --git a/node-helpers/.gitignore b/node-helpers/.gitignore new file mode 100644 index 0000000..1521c8b --- /dev/null +++ b/node-helpers/.gitignore @@ -0,0 +1 @@ +dist diff --git a/node-helpers/dev-server-sse/index.ts b/node-helpers/dev-server-sse/index.ts new file mode 100644 index 0000000..6628021 --- /dev/null +++ b/node-helpers/dev-server-sse/index.ts @@ -0,0 +1,84 @@ +import path from 'node:path'; +import fs from 'node:fs/promises'; +import { setTimeout } from 'node:timers/promises'; +import url from 'node:url'; + +import localIpUrl from 'local-ip-url'; +import { parseGameStateFromAsyncIterator, InitOptions } from 'snakepipe'; + +import { makeServer } from './server.js' + +/** + * `__dirname` doesn't exist in esm, you need to create it. + * + * We could improve the code bellow with just `import.meta.dirname` - it needs at least node 20.11. + * For the moment keeping retrocompatibility with all node 20 versions. + * + * Source: https://nodejs.org/docs/v20.11.0/api/esm.html#importmetadirname + */ +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); // __dirname for esm + +const ExitCodeUsageError = 64; // The command was used incorrectly, e.g., with the wrong number of arguments, a bad flag, a bad syntax in a parameter, etc. + +if (process.argv.length < 3) { + console.log("You must pass the path of the file containing the recording of a game."); + process.exit(ExitCodeUsageError); +} +if (process.argv.length > 3) { + console.log("Too many arguments passed"); + process.exit(ExitCodeUsageError); +} + +const [filePathOfGameRecording] = process.argv.slice(2, 3); + +let resolvedFilePathOfGameRecording: string | null = null; + +if (path.isAbsolute(filePathOfGameRecording)) { + resolvedFilePathOfGameRecording = filePathOfGameRecording +} else { + resolvedFilePathOfGameRecording = path.resolve(process.cwd(), filePathOfGameRecording); +} + +main(resolvedFilePathOfGameRecording); + +async function* infiniteAsyncGeneratorFromArrayString(input: Array, delay = 120) { + let currentIndex = 0; + yield input[currentIndex]; + while (true) { + currentIndex++; + if (input[currentIndex]?.trim()) { + await setTimeout(delay); + yield input[currentIndex] + } + else { + // loop back + currentIndex = 1; + } + } +} + +function makeInitOptionWithLocalIp(initOptions: InitOptions, port: number): InitOptions { + const localIp = localIpUrl(); + return { + ...initOptions, + metadatas: { + ...(initOptions.metadatas || {}), + 'render-browser-host': `http://${localIp}:${port}` + } + } +} + +async function main(resolvedFilePathOfGameRecording: string, port = 8080) { + const staticFolder = path.resolve(__dirname, '../..', 'static'); + const fileContent = + (await fs.readFile(resolvedFilePathOfGameRecording)) + .toString() + .split('\n') + .filter(Boolean); + const asyncGenerator = infiniteAsyncGeneratorFromArrayString(fileContent) + const { options, lines } = await parseGameStateFromAsyncIterator(asyncGenerator); + const initOptionsWithLocalIp = makeInitOptionWithLocalIp(options, port); + makeServer({ options: initOptionsWithLocalIp, lines }, staticFolder).listen({ port, host: "0.0.0.0" }).then(() => { + console.log(`Listening on ${initOptionsWithLocalIp.metadatas?.['render-browser-host']}`); + }); +} diff --git a/node-helpers/dev-server-sse/server.ts b/node-helpers/dev-server-sse/server.ts new file mode 100644 index 0000000..e042222 --- /dev/null +++ b/node-helpers/dev-server-sse/server.ts @@ -0,0 +1,65 @@ +import EventEmitter from 'node:events'; +import Fastify from 'fastify' +import { FastifySSEPlugin } from "fastify-sse-v2"; +import fastifyStatic from '@fastify/static'; + +import { parseGameStateFromAsyncIterator, Game } from 'snakepipe'; + +type Input = Awaited> + +function makeEventEmitterFromAsyncGenerator(lines: () => AsyncGenerator) { + const myEmitter = new EventEmitter(); + + const clientsConnected = new Set(); + + myEmitter.on("connect", (reqId) => { + clientsConnected.add(reqId); + }); + + myEmitter.on("disconnect", (reqId) => { + clientsConnected.delete(reqId); + }); + + const iterator = lines(); + (async function () { + while (true) { + const nextLine = await iterator.next(); + if (!nextLine.done && nextLine.value) { + myEmitter.emit("line", nextLine.value) + } + } + })() + + return myEmitter; +} + +export function makeServer(input: Input, staticFolder: string) { + const gameEvents = makeEventEmitterFromAsyncGenerator(input.lines); + + const server = Fastify({ + logger: false + }); + + server.register(fastifyStatic, { + root: staticFolder + }) + + server.register(FastifySSEPlugin); + server.get("/events", function (req, res) { + function listener(line: Game) { + res.sse({ data: JSON.stringify(line) }); + } + req.raw.on('close', () => { + gameEvents.off("line", listener); // if used in production with eavy traffic, consider increasing `emitter.setMaxListeners()` (currently up to 11 clients in parallel) + res.sseContext.source.end(); + }) + res.sse({ data: "connected" }); + gameEvents.on("line", listener); + }); + + server.get('/init-options', async function handler(request, reply) { + return input.options; + }); + + return server; +} diff --git a/node-helpers/package.json b/node-helpers/package.json new file mode 100644 index 0000000..ceec01a --- /dev/null +++ b/node-helpers/package.json @@ -0,0 +1,24 @@ +{ + "name": "node-helpers", + "version": "0.1.0", + "description": "Set of tools for better development of JavaScript parts.", + "type": "module", + "scripts": { + "build": "tsc", + "dl-recorded-party": "curl -L https://github.com/topheman/snake-pipe-rust/releases/download/v1.1.0/snakepipe-output -o /tmp/snakepipe-output", + "dev-server-sse": "npm run dev-server-sse:file /tmp/snakepipe-output", + "dev-server-sse:file": "node ./dist/index.js", + "setup": "npm run dl-recorded-party" + }, + "author": "Christophe Rosset", + "devDependencies": { + "local-ip-url": "^1.0.10", + "typescript": "^5.3.3" + }, + "dependencies": { + "@fastify/static": "^7.0.1", + "fastify": "^4.26.1", + "fastify-sse-v2": "^3.1.2", + "snakepipe": "latest" + } +} diff --git a/node-helpers/tsconfig.json b/node-helpers/tsconfig.json new file mode 100644 index 0000000..0589e85 --- /dev/null +++ b/node-helpers/tsconfig.json @@ -0,0 +1,101 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "NodeNext", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b379266 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1348 @@ +{ + "name": "snake-pipe-rust", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "snake-pipe-rust", + "version": "0.1.0", + "workspaces": [ + "node-helpers" + ], + "devDependencies": { + "@types/node": "^20.11.24" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz", + "integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz", + "integrity": "sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@fastify/send": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz", + "integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==", + "dependencies": { + "@lukeed/ms": "^2.0.1", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "2.0.0", + "mime": "^3.0.0" + } + }, + "node_modules/@fastify/static": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.1.tgz", + "integrity": "sha512-i1p/nELMknAisNfnjo7yhfoUOdKzA+n92QaMirv2NkZrJ1Wl12v2nyTYlDwPN8XoStMBAnRK/Kx6zKmfrXUPXw==", + "dependencies": { + "@fastify/accept-negotiator": "^1.0.0", + "@fastify/send": "^2.0.0", + "content-disposition": "^0.5.3", + "fastify-plugin": "^4.0.0", + "fastq": "^1.17.0", + "glob": "^10.3.4" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/node": { + "version": "20.11.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", + "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.0.tgz", + "integrity": "sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==", + "dependencies": { + "@fastify/error": "^3.3.0", + "archy": "^1.0.0", + "debug": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/commander": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, + "node_modules/fast-json-stringify": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.12.0.tgz", + "integrity": "sha512-7Nnm9UPa7SfHRbHVA1kJQrGXCRzB7LMlAAqHXQFkEQqueJm1V8owm0FsE/2Do55/4CcdhwiLQERaKomOnKQkyA==", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^2.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-uri": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", + "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==" + }, + "node_modules/fastify": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.1.tgz", + "integrity": "sha512-tznA/G55dsxzM5XChBfcvVSloG2ejeeotfPPJSFaWmHyCDVGMpvf3nRNbsCb/JTBF9RmQFBfuujWt3Nphjesng==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.3.0", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^8.17.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, + "node_modules/fastify-sse-v2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fastify-sse-v2/-/fastify-sse-v2-3.1.2.tgz", + "integrity": "sha512-eEgxBv04wWtrIupDKw65DtdI8Q4XJA3IGstefkSU8qgDlUlexc7+cixxowxlSGj1pz/HC3QsKGLhna+fb7snOQ==", + "dependencies": { + "fastify-plugin": "^4.3.0", + "it-pushable": "^1.4.2", + "it-to-stream": "^1.0.0" + }, + "peerDependencies": { + "fastify": ">=4" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.1.0.tgz", + "integrity": "sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/get-iterator": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-iterator/-/get-iterator-1.0.2.tgz", + "integrity": "sha512-v+dm9bNVfOYsY1OrhaCrmyOcYoSeVvbt+hHZ0Au+T+p1y+0Uyj9aMaGIeUTT6xdpRbWzDeYKvfOslPhggQMcsg==" + }, + "node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/it-pushable": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/it-pushable/-/it-pushable-1.4.2.tgz", + "integrity": "sha512-vVPu0CGRsTI8eCfhMknA7KIBqqGFolbRx+1mbQ6XuZ7YCz995Qj7L4XUviwClFunisDq96FdxzF5FnAbw15afg==", + "dependencies": { + "fast-fifo": "^1.0.0" + } + }, + "node_modules/it-to-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/it-to-stream/-/it-to-stream-1.0.0.tgz", + "integrity": "sha512-pLULMZMAB/+vbdvbZtebC0nWBTbG581lk6w8P7DfIIIKUfa8FbY7Oi0FxZcFPbxvISs7A9E+cMpLDBc1XhpAOA==", + "dependencies": { + "buffer": "^6.0.3", + "fast-fifo": "^1.0.0", + "get-iterator": "^1.0.2", + "p-defer": "^3.0.0", + "p-fifo": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/light-my-request": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.11.1.tgz", + "integrity": "sha512-KXAh2m6VRlkWCk2KfmHE7tLBXKh30JE0tXUJY4dNxje4oLmPKUqlUfImiEQZLphx+Z9KTQcVv4DjGnJxkVOIbA==", + "dependencies": { + "cookie": "^0.6.0", + "process-warning": "^2.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", + "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==" + }, + "node_modules/local-ip-url": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/local-ip-url/-/local-ip-url-1.0.10.tgz", + "integrity": "sha512-CtwzPEuFFb1EaDvmWFDy7SxnhQyYb7tYZOwr2dvk++ZlJhWZJTWAKZsqyVr5apgY63gVx+e3AMqkmFDnkcQTMA==", + "dev": true, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/node-helpers": { + "resolved": "node-helpers", + "link": true + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-fifo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-fifo/-/p-fifo-1.0.0.tgz", + "integrity": "sha512-IjoCxXW48tqdtDFz6fqo5q1UfFVjjVZe8TC1QRflvNUJtNfCUhxOUw6MOVZhDPjqhSzc26xKdugsO17gmzd5+A==", + "dependencies": { + "fast-fifo": "^1.0.0", + "p-defer": "^3.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/pino": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.19.0.tgz", + "integrity": "sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", + "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "dependencies": { + "ret": "~0.2.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/snakepipe": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/snakepipe/-/snakepipe-0.1.3.tgz", + "integrity": "sha512-E7D5nskAo2ZasFufthjslxrYPBsqwdDRkaed4GmcodhaseM6y8oOWgkTER645yP7AOKpm82vIBHm7VGaE3kqGw==", + "dependencies": { + "commander": "^12.0.0", + "zod": "^3.22.4" + }, + "bin": { + "snakepipe-node": "bin.js" + } + }, + "node_modules/sonic-boom": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.0.tgz", + "integrity": "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node-helpers": { + "version": "0.1.0", + "dependencies": { + "@fastify/static": "^7.0.1", + "fastify": "^4.26.1", + "fastify-sse-v2": "^3.1.2", + "snakepipe": "latest" + }, + "devDependencies": { + "local-ip-url": "^1.0.10", + "typescript": "^5.3.3" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4fe3cf5 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "snake-pipe-rust", + "version": "0.1.0", + "private": true, + "description": "You only need this part for a better experience with js development, not mandadtory.", + "scripts": { + "build": "npm run --workspace=node-helpers build", + "setup": "npm run --workspace=node-helpers setup", + "dev-server-sse": "npm run --workspace=node-helpers dev-server-sse", + "dev-server-sse:file": "npm run --workspace=node-helpers dev-server-sse:file" + }, + "author": "Christophe Rosset", + "workspaces": [ + "node-helpers" + ], + "devDependencies": { + "@types/node": "^20.11.24" + } +} diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000..da29233 --- /dev/null +++ b/src/common.rs @@ -0,0 +1,195 @@ +use indexmap::map::IndexMap; // we need IndexMap to have deterministic order of keys when `.iter()` +use std::collections::HashMap; + +use crate::input::SizeOption; +use clap::crate_version; + +/// Returns the name of the crate with the version in the `Cargo.toml` +pub fn format_version_to_display() -> String { + format!("snakepipe@{}(rust)", crate_version!()) +} + +/// Takes in [`crate::input::InitOptions::features_with_version`] and extracts a HashMap with +/// - keys: versions +/// - values: vector of string of features +/// +/// Example: +/// +/// Input (as [`std::collections::HashMap`]) +/// ```json +/// { +/// "throttle": "snakepipe@1.0.0(node)", +/// "render": "snakepipe@1.0.0(rust)", +/// "gamestate": "snakepipe@1.0.0(rust)" +/// } +/// ``` +/// +/// Output (as [`indexmap::map::IndexMap>`]) +/// ```json +/// { +/// "snakepipe@1.0.0(rust)": [ +/// "gamestate", +/// "render" +/// ], +/// "snakepipe@1.0.0(node)": [ +/// "throttle" +/// ] +/// } +/// ``` +pub fn extract_versions_with_features( + features_with_version: HashMap, +) -> IndexMap> { + let mut versions_with_features: IndexMap> = IndexMap::new(); + features_with_version.iter().for_each(|(feature, version)| { + if versions_with_features.contains_key(version) { + versions_with_features + .entry(version.to_string()) + .and_modify(|features| features.push(feature.to_string())); + } else { + versions_with_features.insert(version.to_string(), vec![feature.to_string()]); + } + }); + versions_with_features.values_mut().for_each(|features| { + features.sort(); + }); + return versions_with_features; +} + +/// Takes in the output of [`extract_versions_with_features`] and formats it in a string +/// +/// Example: +/// +/// Input +/// ```json +/// { +/// "snakepipe@1.0.0(rust)": [ +/// "gamestate", +/// "render" +/// ], +/// "snakepipe@1.0.0(node)": [ +/// "throttle" +/// ] +/// } +/// ``` +/// +/// Ouput +/// +/// `snakepipe@1.0.0(rust): gamestate/render - snakepipe@1.0.0(node): throttle` +pub fn format_version_with_features( + versions_with_features: IndexMap>, +) -> String { + if versions_with_features.len() == 1 { + if let Some((version, _)) = versions_with_features.iter().next() { + return version.to_string(); + } + return "Unknown version".to_string(); + } + let couple_version_features: Vec = versions_with_features + .iter() + .map(|(version, features)| format!("{}: {}", version, features.join("/"))) + .collect(); + return couple_version_features.join(" - "); +} + +/// Takes in [`crate::input::InitOptions::features_with_version`] and formats it to a string +/// +/// Composes [extract_versions_with_features] and [format_version_with_features]. +pub fn format_version(features_with_version: HashMap) -> String { + return format_version_with_features(extract_versions_with_features(features_with_version)); +} + +pub fn format_metadatas( + metadatas: HashMap, + _frame_duration: u32, + _size: SizeOption, +) -> String { + let mut result: Vec = Vec::new(); + if let Some(value) = metadatas.get("throttled") { + if value == "on" { + result.push("Record mode".to_string()); + } + } + if let Some(value) = metadatas.get("render-browser-host") { + result.push(format!("Mirrored on {}", value)); + } + return result.join(" / "); +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + + #[test] + fn should_have_only_one_key_with_all_features_if_the_same_version_evrywhere() { + let mut version = HashMap::new(); + version.insert("gamestate".to_string(), "snakepipe@1.0.0(rust)".to_string()); + version.insert("throttle".to_string(), "snakepipe@1.0.0(rust)".to_string()); + version.insert("render".to_string(), "snakepipe@1.0.0(rust)".to_string()); + let mut result = IndexMap::new(); + result.insert( + "snakepipe@1.0.0(rust)".to_string(), + vec![ + "gamestate".to_string(), + "render".to_string(), + "throttle".to_string(), + ], + ); + assert_eq!(extract_versions_with_features(version), result); + } + + #[test] + fn should_extract_all_existing_versions() { + let mut version = HashMap::new(); + version.insert("gamestate".to_string(), "snakepipe@1.0.0(rust)".to_string()); + version.insert("throttle".to_string(), "snakepipe@1.0.0(node)".to_string()); + version.insert("render".to_string(), "snakepipe@1.0.0(rust)".to_string()); + let mut result = IndexMap::new(); + result.insert( + "snakepipe@1.0.0(rust)".to_string(), + vec!["gamestate".to_string(), "render".to_string()], + ); + result.insert( + "snakepipe@1.0.0(node)".to_string(), + vec!["throttle".to_string()], + ); + assert_eq!(extract_versions_with_features(version), result); + } + + #[test] + fn should_not_show_features_if_they_all_have_the_same_version() { + let mut versions_with_features = IndexMap::new(); + versions_with_features.insert( + "snakepipe@1.0.0(rust)".to_string(), + vec![ + "gamestate".to_string(), + "render".to_string(), + "throttle".to_string(), + ], + ); + let expected = "snakepipe@1.0.0(rust)"; + assert_eq!( + format_version_with_features(versions_with_features), + expected.to_string() + ); + } + + #[test] + fn should_show_features_if_version_differs() { + let mut versions_with_features = IndexMap::new(); + versions_with_features.insert( + "snakepipe@1.0.0(rust)".to_string(), + vec!["gamestate".to_string(), "render".to_string()], + ); + versions_with_features.insert( + "snakepipe@1.0.0(node)".to_string(), + vec!["throttle".to_string()], + ); + let expected = "snakepipe@1.0.0(rust): gamestate/render - snakepipe@1.0.0(node): throttle"; + assert_eq!( + format_version_with_features(versions_with_features), + expected.to_string() + ); + } +} diff --git a/src/gamestate/game.rs b/src/gamestate/game.rs new file mode 100644 index 0000000..22f5a8d --- /dev/null +++ b/src/gamestate/game.rs @@ -0,0 +1,170 @@ +use crossterm::event::KeyModifiers; +use rand::Rng; +use serde::Serialize; + +use crate::gamestate::physics::{Direction, Position}; +use crate::gamestate::snake::Snake; + +fn calc_random_pos(width: u32, height: u32) -> Position { + let mut rng = rand::thread_rng(); + + Position { + x: rng.gen_range(0..width as i32), + y: rng.gen_range(0..height as i32), + } +} + +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum GameState { + Paused, + Over, + Running, +} + +#[derive(Debug, Serialize)] +pub struct Game { + snake: Snake, + fruit: Position, + #[serde(skip)] + size: (u32, u32), + #[serde(skip)] + frame_duration: f64, + #[serde(skip)] + waiting_time: f64, + score: u32, + pub state: GameState, + #[serde(skip)] + initial_snake_length: u32, +} + +impl Game { + pub fn new(width: u32, height: u32, frame_duration: f64, snake_length: u32) -> Self { + Self { + snake: Snake::new(calc_random_pos(width, height), snake_length), + fruit: calc_random_pos(width, height), + size: (width, height), + frame_duration, + waiting_time: 0.0, + score: 0, + state: GameState::Paused, + initial_snake_length: snake_length, + } + } + + pub fn start(&mut self) { + self.state = GameState::Running; + } + + pub fn pause(&mut self) { + self.state = GameState::Paused; + } + + pub fn resume(&mut self) { + self.state = GameState::Running; + } + + pub fn restart(&mut self) { + self.snake = Snake::new( + calc_random_pos(self.size.0, self.size.1), + self.initial_snake_length, + ); + self.fruit = calc_random_pos(self.size.0, self.size.1); + self.score = 0; + self.state = GameState::Running; + } + + /// returns true if the state has been updated because it was time to + pub fn update(&mut self, delta_time: f64) -> bool { + self.waiting_time += delta_time; + + if self.waiting_time > self.frame_duration && self.state != GameState::Over { + self.waiting_time = 0.0; + + if self.state == GameState::Paused || self.state == GameState::Over { + return true; + } + + if !self.snake.is_tail_overlapping() && !self.snake.will_tail_overlap() { + if *self.snake.get_head_pos() == self.fruit { + self.snake.grow(); + self.snake.update(self.size.0, self.size.1); + self.fruit = calc_random_pos(self.size.0, self.size.1); + self.calc_score(); + } else { + self.snake.update(self.size.0, self.size.1); + } + } else { + self.state = GameState::Over; + } + return true; + } + return false; + } + + pub fn key_down(&mut self, event: crossterm::event::Event) -> Option<()> { + use crossterm::event::{Event, KeyCode, KeyEvent}; + + match event { + Event::Key(KeyEvent { + code: KeyCode::Char('p'), + .. + }) => { + if self.state != GameState::Paused { + self.pause(); + } else { + self.resume(); + } + Some(()) + } + Event::Key(KeyEvent { + code: KeyCode::Char('r'), + .. + }) => { + self.restart(); + Some(()) + } + Event::Key(KeyEvent { + code: KeyCode::Left, + .. + }) => { + self.snake.set_dir(Direction::Left); + Some(()) + } + Event::Key(KeyEvent { + code: KeyCode::Right, + .. + }) => { + self.snake.set_dir(Direction::Right); + Some(()) + } + Event::Key(KeyEvent { + code: KeyCode::Up, .. + }) => { + self.snake.set_dir(Direction::Up); + Some(()) + } + Event::Key(KeyEvent { + code: KeyCode::Down, + .. + }) => { + self.snake.set_dir(Direction::Down); + Some(()) + } + Event::Key(KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + .. + }) => None, + _ => Some(()), + } + } + + pub fn get_score(&self) -> u32 { + self.score + } + + fn calc_score(&mut self) { + self.score = (self.snake.get_len() * 10) as u32 + } +} diff --git a/src/gamestate/mod.rs b/src/gamestate/mod.rs new file mode 100644 index 0000000..085ae13 --- /dev/null +++ b/src/gamestate/mod.rs @@ -0,0 +1,49 @@ +pub mod game; +pub mod physics; +pub mod snake; + +use std::time::{Duration, Instant}; + +use crossterm::event::{poll, read}; + +use crate::gamestate::game::GameState; +use crate::input::InitOptions; + +/** + * This function is the update loop. + * It keeps track of the user inputs via the keyboard. + * It runs forever and returns if ctrl+c is hit. + */ +pub fn run(options: InitOptions) -> std::io::Result<()> { + println!("{}\r", serde_json::to_string(&options).unwrap()); + let mut main = game::Game::new( + options.size.width, + options.size.height, + options.frame_duration as f64, + options.snake_length, + ); + let mut last_loop_duration: Duration = Duration::new(0, 0); + main.start(); + let mut prev_state = main.state.clone(); + loop { + let start = Instant::now(); + if poll(Duration::from_millis(20))? { + let event = read()?; + + // return Ok(()) when ctrl+c is hit + if let None = main.key_down(event) { + return Ok(()); + } + } + if main.update(last_loop_duration.as_millis() as f64) { + if main.state == GameState::Running + || main.state == GameState::Over + || main.state == GameState::Paused && prev_state == GameState::Running + { + println!("{}\r", serde_json::to_string(&main).unwrap()); + } + prev_state = main.state.clone(); + } + last_loop_duration = start.elapsed(); + } +} diff --git a/src/gamestate/physics.rs b/src/gamestate/physics.rs new file mode 100644 index 0000000..65f1a3e --- /dev/null +++ b/src/gamestate/physics.rs @@ -0,0 +1,38 @@ +use serde::Serialize; + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub struct Position { + pub x: i32, + pub y: i32, +} + +impl Position { + pub fn move_to_dir(&mut self, dir: Direction) { + match dir { + Direction::Up => self.y -= 1, + Direction::Down => self.y += 1, + Direction::Left => self.x -= 1, + Direction::Right => self.x += 1, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum Direction { + Up, + Right, + Down, + Left, +} + +impl Direction { + pub fn opposite(&self) -> Direction { + match *self { + Direction::Up => Direction::Down, + Direction::Right => Direction::Left, + Direction::Down => Direction::Up, + Direction::Left => Direction::Right, + } + } +} diff --git a/src/gamestate/snake.rs b/src/gamestate/snake.rs new file mode 100644 index 0000000..b7d8763 --- /dev/null +++ b/src/gamestate/snake.rs @@ -0,0 +1,121 @@ +use serde::Serialize; +use std::collections::LinkedList; + +use crate::gamestate::physics::{Direction, Position}; + +#[derive(Debug, Serialize)] +pub struct Snake { + direction: Direction, + head: Position, + tail: LinkedList, + #[serde(skip)] + updated_tail_pos: bool, + #[serde(skip)] + initial_length: u32, +} + +impl Snake { + pub fn new(head: Position, snake_length: u32) -> Self { + let (x, y) = (head.x, head.y); + let mut tail = LinkedList::new(); + + for i in 1..(snake_length + 1) { + tail.push_back(Position { x, y: y - i as i32 }); + } + + Self { + direction: Direction::Down, + head: Position { x, y }, + tail, + updated_tail_pos: false, + initial_length: snake_length, + } + } + + pub fn update(&mut self, height: u32, width: u32) { + if self.tail.len() > 0 { + self.tail.push_front(self.head.clone()); + self.tail.pop_back(); + } + + match self.direction { + Direction::Up => self.head.y -= 1, + Direction::Right => self.head.x += 1, + Direction::Down => self.head.y += 1, + Direction::Left => self.head.x -= 1, + } + + if self.head.x >= height as i32 { + self.head.x = 0; + } else if self.head.y >= width as i32 { + self.head.y = 0; + } else if self.head.y < 0 { + self.head.y = (width - 1) as i32; + } else if self.head.x < 0 { + self.head.x = (height - 1) as i32; + } + + self.updated_tail_pos = true; + } + + pub fn set_dir(&mut self, dir: Direction) { + if dir == self.direction.opposite() || !self.updated_tail_pos { + return; + } + + self.direction = dir; + self.updated_tail_pos = false; + } + + pub fn get_head_pos(&self) -> &Position { + &self.head + } + + pub fn get_len(&self) -> usize { + &self.tail.len() - self.initial_length as usize + } + + pub fn is_tail_overlapping(&self) -> bool { + for pos in self.tail.iter() { + if *pos == self.head { + return true; + } + } + + false + } + + pub fn will_tail_overlap(&self) -> bool { + let next = self.next_head_pos(); + + for pos in self.tail.iter() { + if *pos == next { + return true; + } + } + + false + } + + pub fn grow(&mut self) { + let last = match self.tail.back() { + Some(pos) => pos.clone(), + None => self.head.clone(), + }; + + self.tail.push_back(last); + } + + fn next_head_pos(&self) -> Position { + let mut pos = self.head.clone(); + + match self.direction { + Direction::Up => pos.y -= 1, + Direction::Left => pos.x -= 1, + Direction::Down => pos.y += 1, + Direction::Right => pos.x += 1, + } + + pos + } +} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..a9f07fa --- /dev/null +++ b/src/input.rs @@ -0,0 +1,152 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::io::{stdin, BufRead, Lines}; + +// options + +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub struct SizeOption { + pub width: u32, + pub height: u32, +} + +/// Holds the options that were passed to the cli with a flag +/// that are relevent for rendering the game. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct InitOptions { + pub frame_duration: u32, + #[serde(skip)] + pub snake_length: u32, + pub size: SizeOption, + pub features_with_version: std::collections::HashMap, + pub metadatas: std::collections::HashMap, +} + +// gamestate + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "lowercase")] +pub enum Direction { + Up, + Right, + Down, + Left, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Position { + pub x: i32, + pub y: i32, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Snake { + pub direction: Direction, + pub head: Position, + pub tail: Vec, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum GameState { + Paused, + Over, + Running, +} + +impl fmt::Display for GameState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let str = match self { + Self::Over => "Game Over", + Self::Paused => "Paused", + Self::Running => "Running", + }; + write!(f, "{}", str) + } +} + +/// Holds the state of the game at any time +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Game { + pub snake: Snake, + pub fruit: Position, + pub score: u32, + pub state: GameState, +} + +/// Accepts the iterator from [`std::io::stdin()`]`.line()` +/// - parses the first line into `options` as [`InitOptions`] +/// - returns an iterator of [`Game`] inside `lines` (already parsed) +/// +/// Used by [`parse_gamestate`] under the hood. +pub struct Input { + pub options: InitOptions, + pub lines: Box>, // std::io::Lines, //Lines, +} + +impl Input { + /// Creates a input from a buffer (could be from [`std::io::stdin()`]`.line()`) + pub fn new( + mut lines: Lines, + ) -> Result> { + match lines.next() { + Some(Ok(first_line)) => { + let options: InitOptions = serde_json::from_str(&first_line)?; + // flat_map keeps Some and extracts their values while removing Err - we ignore parse errors on lines / we dont panic on it + let parsed_lines = lines.flat_map(|result_line| match result_line { + Ok(line) => match serde_json::from_str::(&line) { + Ok(parsed_line) => Some(parsed_line), + Err(_) => None, + }, + Err(_) => None, + }); + Ok(Self { + options, + lines: Box::new(parsed_lines), + }) + } + None => Err("Buffer is empty".into()), + Some(Err(e)) => Err(e.into()), + } + } +} + +/// Parses the stdin containing the gamestate +/// +/// Example: +/// ``` +/// use snakepipe::input::{parse_gamestate, Game}; +/// +/// fn main() -> () { +/// match parse_gamestate() { +/// Ok(input) => { +/// println!( +/// "Frame duration {}, Snake length {}, Level {}x{}", +/// input.options.frame_duration, +/// input.options.snake_length, +/// input.options.size.width, +/// input.options.size.height +/// ); +/// for parsed_line in input.lines { +/// do_something(parsed_line); +/// } +/// } +/// Err(e) => { +/// println!("Error occurred while parsing stdin: \"{}\"", e); +/// } +/// } +/// } +/// +/// fn do_something(parsed_line: Game) { +/// println!("Snake head position {:?}", parsed_line.snake.head) +/// } +/// ``` +/// +/// If you want to parse from elsewhere than stdin, you can use [Input] +pub fn parse_gamestate() -> Result> { + // todo couldn't find how to peek into the input (to know if it comes from `snake gamestate` or `cat /some-file`), without consuming it + // so we'll show a "Replay" message when `gamestate throttle` is used in the pipeline (even if it could only be used to throttle directly `gamestate`) + let lines = stdin().lines(); + Input::new(lines) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..24fea1d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +pub mod common; +#[doc(hidden)] +pub mod gamestate; +pub mod input; +#[doc(hidden)] +pub mod pipeline; +#[doc(hidden)] +pub mod render; +#[doc(hidden)] +pub mod render_browser; +#[doc(hidden)] +pub mod stream_sse; +#[doc(hidden)] +pub mod throttle; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b46c8f1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,166 @@ +use clap::{Parser, Subcommand}; +use crossterm; + +use snakepipe::common::format_version_to_display; +use snakepipe::gamestate::run as gamestate_run; +use snakepipe::input::{InitOptions, SizeOption}; +use snakepipe::pipeline::{generate_command as pipeline_generate_command, Pipeline}; +use snakepipe::render::run as render_run; +use snakepipe::render_browser::common::port_is_available; +use snakepipe::render_browser::run as render_browser_run; +use snakepipe::stream_sse::run as stream_sse_run; +use snakepipe::throttle::run as throttle_run; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +const DEFAULT_WIDTH: u32 = 25; +const DEFAULT_HEIGHT: u32 = 25; + +#[derive(Subcommand)] +enum Commands { + /// Accepts user inputs (arrow keys to control the snake) and outputs the state of the game to stdout + Gamestate { + /// in ms + #[arg(long, default_value_t = 120)] + frame_duration: u32, + /// default 25 + #[arg(long)] + width: Option, + /// default 25 + #[arg(long)] + height: Option, + #[arg(long, default_value_t = 2)] + snake_length: u32, + #[arg(long, default_value_t = false)] + fit_terminal: bool, + }, + /// Reads gamestate from stdin and renders the game on your terminal + Render, + /// Reads stdin line by line and outputs each line on stdout each `frame_duration` ms (usefull for replaying a file) + Throttle { + /// in ms + #[arg(long, default_value_t = 120)] + frame_duration: u32, + #[arg(long)] + loop_infinite: bool, + }, + /// Let's you render the game in your browser at http://localhost:8080 by spawning a server and sending stdin via server-sent events to a JavaScript renderer + RenderBrowser { + #[arg(long, default_value_t = 8080)] + port: u16, + }, + /// Connects to the server spawned by `render-browser` and streams server-sent events back to the terminal + StreamSse { + #[arg(long, default_value = "http://localhost:8080")] + address: String, + }, + /// Prints out some common pipelines, so that you can copy/paste them to execute (you can pipe to `pbcopy`) + #[command(arg_required_else_help = true)] + Pipeline(PipelineArgs), +} + +#[derive(Parser)] +pub struct PipelineArgs { + #[command(subcommand)] + sub: Option, + #[arg(long)] + list: bool, +} + +struct CliOptions<'a> { + frame_duration: &'a u32, + width: &'a Option, + height: &'a Option, + snake_length: &'a u32, + fit_terminal: &'a bool, +} + +impl Into for CliOptions<'_> { + fn into(self) -> InitOptions { + let size: SizeOption; + if self.width.is_some() && self.height.is_some() { + size = SizeOption { + width: self.width.unwrap_or(DEFAULT_WIDTH), + height: self.height.unwrap_or(DEFAULT_HEIGHT), + } + } else if self.width.is_some() { + size = SizeOption { + width: self.width.unwrap_or(DEFAULT_WIDTH), + height: self.height.unwrap_or(DEFAULT_HEIGHT), + } + } else if self.fit_terminal.eq(&true) { + let (width, height) = crossterm::terminal::size() + .unwrap_or((DEFAULT_WIDTH as u16 + 2, DEFAULT_HEIGHT as u16 + 6)); + size = SizeOption { + width: width as u32 - 2, // 2 borders + height: height as u32 - 6, // 2 borders + 4 lines of score/etc ... + } + } else { + size = SizeOption { + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + } + } + let mut features_with_version = std::collections::HashMap::new(); + features_with_version.insert("gamestate".to_string(), format_version_to_display()); + let metadatas = std::collections::HashMap::new(); + return InitOptions { + frame_duration: *self.frame_duration, + snake_length: *self.snake_length, + size, + features_with_version: features_with_version, + metadatas, + }; + } +} + +fn main() { + let cli = Cli::parse(); + + match &cli.command { + Commands::Gamestate { + frame_duration, + width, + height, + snake_length, + fit_terminal, + } => { + let cli_options = CliOptions { + frame_duration: frame_duration, + width: width, + height: height, + snake_length: snake_length, + fit_terminal: fit_terminal, + }; + let game_options: InitOptions = cli_options.into(); + + // enable_raw_mode()?; // https://docs.rs/crossterm/0.27.0/crossterm/terminal/index.html#raw-mode + let _ = crossterm::terminal::enable_raw_mode(); + let _ = gamestate_run(game_options); // this function returns when ctrl+c is hit + let _ = crossterm::terminal::disable_raw_mode(); + std::process::exit(130); // todo handle other signals ? + } + Commands::Render => { + render_run(); + } + Commands::Throttle { + frame_duration, + loop_infinite, + } => throttle_run(*frame_duration, *loop_infinite), + Commands::RenderBrowser { port } => { + if port_is_available(*port) { + return render_browser_run(*port); + } + eprintln!("Error: port {} already in use", port); + std::process::exit(exitcode::UNAVAILABLE); + } + Commands::StreamSse { address } => stream_sse_run(address.to_string()), + Commands::Pipeline(cmd) => pipeline_generate_command(cmd.sub, cmd.list, ""), + } +} diff --git a/src/pipeline.rs b/src/pipeline.rs new file mode 100644 index 0000000..7dda709 --- /dev/null +++ b/src/pipeline.rs @@ -0,0 +1,75 @@ +use clap::Subcommand; +use owo_colors::OwoColorize; + +#[derive(Subcommand, Copy, Clone)] +pub enum Pipeline { + /// Play in the terminal + Play, + /// Record a party in the terminal + Record, + /// Replay a party you recorded in the terminal + Replay, + /// Play and share a party via a shared file in realtime + FilePlay, + /// Render the party you are sharing through a file in realtime + FileWatch, + /// Play and share a party through an http server + HttpPlay, + /// Render the party you shared through the http server, in the terminal + HttpWatch, +} + +fn print_formatted_pipeline(pipeline: &str, prefix: &str) { + if prefix.is_empty() { + println!("{}", pipeline); + } else { + println!(" {:12}{}", prefix.bold(), pipeline); + } +} + +pub fn generate_command(pipeline: Option, list: bool, prefix: &str) { + match pipeline { + Some(Pipeline::Play) => { + print_formatted_pipeline("snakepipe gamestate|snakepipe render", prefix); + } + Some(Pipeline::Record) => print_formatted_pipeline( + "snakepipe gamestate|tee /tmp/snakepipe-output|snakepipe render", + prefix, + ), + Some(Pipeline::Replay) => print_formatted_pipeline( + "cat /tmp/snakepipe-output|snakepipe throttle|snakepipe render", + prefix, + ), + Some(Pipeline::FilePlay) => print_formatted_pipeline( + "snakepipe gamestate|tee /tmp/snakepipe-output|snakepipe render", + prefix, + ), + Some(Pipeline::FileWatch) => print_formatted_pipeline( + "cat /dev/null > /tmp/snakepipe-output && tail -f /tmp/snakepipe-output|snakepipe render", + prefix, + ), + Some(Pipeline::HttpPlay) => print_formatted_pipeline( + "snakepipe gamestate|snakepipe render-browser|snakepipe render", + prefix, + ), + Some(Pipeline::HttpWatch) => { + print_formatted_pipeline("snakepipe stream-sse|snakepipe render", prefix) + } + None => { + if list { + println!("{}", "List of pipelines:".bold().underline()); + generate_command(Some(Pipeline::Play), false, "play"); + generate_command(Some(Pipeline::Record), false, "record"); + generate_command(Some(Pipeline::Replay), false, "replay"); + generate_command(Some(Pipeline::FilePlay), false, "file-play"); + generate_command(Some(Pipeline::FileWatch), false, "file-watch"); + generate_command(Some(Pipeline::HttpPlay), false, "http-play"); + generate_command(Some(Pipeline::HttpWatch), false, "http-watch"); + println!( + "\nTo copy a pipeline, run: {}", + "snakepipe pipeline |pbcopy".bold() + ); + } + } + } +} diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..1535a0b --- /dev/null +++ b/src/render.rs @@ -0,0 +1,195 @@ +use ctrlc; +use std::io::Write; + +use crate::common::{format_metadatas, format_version}; +use crate::input::{parse_gamestate, Direction as InputDirection, Game, GameState}; +use array2d::Array2D; +use crossterm::{cursor, queue, style, terminal}; + +#[derive(Clone, Debug)] +enum Direction { + Up, + Right, + Down, + Left, +} + +impl From for Direction { + fn from(value: InputDirection) -> Self { + match value { + InputDirection::Up => Direction::Up, + InputDirection::Down => Direction::Down, + InputDirection::Left => Direction::Left, + InputDirection::Right => Direction::Right, + } + } +} + +#[derive(Clone, Debug)] +enum Point { + Head(Direction), + Tail, + Fruit, + Nothing, +} + +#[derive(Debug)] +struct RenderGrid { + data: Array2D, +} + +impl RenderGrid { + fn new(width: u32, height: u32) -> Self { + RenderGrid { + data: Array2D::filled_with(Point::Nothing, height as usize, width as usize), + } + } + fn set(&mut self, x: usize, y: usize, point: Point) { + let _ = self.data.set(y, x, point); + } +} + +pub fn run() { + match parse_gamestate() { + Ok(input) => { + ctrlc::set_handler(|| { + // cleanup on ctrl+c + queue!( + std::io::stdout(), + cursor::RestorePosition, + terminal::Clear(terminal::ClearType::FromCursorDown), + cursor::Show, + ) + .unwrap(); + std::process::exit(130); + }) + .expect("Could not send signal on channel."); + + let version = format_version(input.options.features_with_version); + let formatted_metadatas = format_metadatas( + input.options.metadatas, + input.options.frame_duration, + input.options.size, + ); + let formatted_metadatas = if formatted_metadatas.is_empty() { + "".to_string() + } else { + format!(" - {}", formatted_metadatas) + }; + + let mut stdout = std::io::stdout(); + queue!( + stdout, + terminal::Clear(terminal::ClearType::All), + cursor::Hide, + cursor::MoveTo(0, 0), + cursor::SavePosition, + ) + .unwrap(); + for parsed_line in input.lines { + let mut grid = RenderGrid::new(input.options.size.width, input.options.size.height); + prepare_grid(&mut grid, parsed_line.clone()); + render_frame( + &grid, + &version, + &formatted_metadatas, + input.options.size.width, + parsed_line.score, + parsed_line.state, + &mut stdout, + ); + stdout.flush().unwrap(); + } + // once there is no more lines (maybe ctrl-c), show the cursor back + queue!( + stdout, + cursor::RestorePosition, + terminal::Clear(terminal::ClearType::FromCursorDown), + cursor::Show, + ) + .unwrap(); + } + Err(e) => { + println!("Error occurred while parsing stdin: \"{}\"", e); + } + } +} + +fn prepare_grid(grid: &mut RenderGrid, game_state: Game) { + let direction: Direction = game_state.snake.direction.into(); + grid.set( + game_state.snake.head.x as usize, + game_state.snake.head.y as usize, + Point::Head(direction.clone()), + ); + game_state.snake.tail.into_iter().for_each(|f| { + grid.set(f.x as usize, f.y as usize, Point::Tail); + }); + grid.set( + game_state.fruit.x as usize, + game_state.fruit.y as usize, + Point::Fruit, + ); +} + +/** + * `` + */ +fn render_line_wrapper(width: u32, top: bool) -> String { + let line = (0..width) + .into_iter() + .fold("".to_string(), |acc, _| format!("{}{}", acc, "-")); + match top { + true => format!("{}{}{}", "\u{250C}", line, "\u{2510}"), + false => format!("{}{}{}", "\u{2514}", line, "\u{2518}"), + } +} + +fn render_frame( + grid: &RenderGrid, + version: &String, + formatted_metadatas: &String, + width: u32, + score: u32, + state: GameState, + stdout: &mut std::io::Stdout, +) { + queue!( + stdout, + cursor::RestorePosition, + style::Print(render_line_wrapper(width, true)), + cursor::MoveToNextLine(1) + ) + .unwrap(); + grid.data.rows_iter().for_each(|row| { + let row_reduced: String = row.into_iter().fold("".to_string(), |row_acc, cell| { + let cell_content = match cell { + Point::Fruit => "F", + Point::Head(_) => "H", + Point::Nothing => "·", + Point::Tail => "T", + }; + format!("{}{}", row_acc, cell_content) + }); + queue!( + stdout, + style::Print(format!("│{}│", row_reduced)), + cursor::MoveToNextLine(1) + ) + .unwrap(); + }); + queue!( + stdout, + style::Print(render_line_wrapper(width, false)), + cursor::MoveToNextLine(1), + style::Print(format!( + "Score: {} - {}{} ", + score, state, formatted_metadatas + )), + cursor::MoveToNextLine(1), + style::Print(format!("[P] Pause [R] Restart [Ctrl+C] Quit")), + cursor::MoveToNextLine(2), + style::Print(format!("{}", version)), + ) + .unwrap(); +} diff --git a/src/render_browser/broadcast.rs b/src/render_browser/broadcast.rs new file mode 100644 index 0000000..25f216f --- /dev/null +++ b/src/render_browser/broadcast.rs @@ -0,0 +1,89 @@ +use std::{sync::Arc, time::Duration}; + +use actix_web::rt::time::interval; +use actix_web_lab::{ + sse::{self, Sse}, + util::InfallibleStream, +}; +use futures_util::future; +use parking_lot::Mutex; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; + +pub struct Broadcaster { + inner: Mutex, +} + +#[derive(Debug, Clone, Default)] +struct BroadcasterInner { + clients: Vec>, +} + +impl Broadcaster { + /// Constructs new broadcaster and spawns ping loop. + pub fn create() -> Arc { + let this = Arc::new(Broadcaster { + inner: Mutex::new(BroadcasterInner::default()), + }); + + Broadcaster::spawn_ping(Arc::clone(&this)); + + this + } + + /// Pings clients every 10 seconds to see if they are alive and remove them from the broadcast + /// list if not. + fn spawn_ping(this: Arc) { + actix_web::rt::spawn(async move { + let mut interval = interval(Duration::from_secs(10)); + + loop { + interval.tick().await; + this.remove_stale_clients().await; + } + }); + } + + /// Removes all non-responsive clients from broadcast list. + async fn remove_stale_clients(&self) { + let clients = self.inner.lock().clients.clone(); + + let mut ok_clients = Vec::new(); + + for client in clients { + if client + .send(sse::Event::Comment("ping".into())) + .await + .is_ok() + { + ok_clients.push(client.clone()); + } + } + + self.inner.lock().clients = ok_clients; + } + + /// Registers client with broadcaster, returning an SSE response body. + pub async fn new_client(&self) -> Sse>> { + let (tx, rx) = mpsc::channel(10); + + tx.send(sse::Data::new("connected").into()).await.unwrap(); + + self.inner.lock().clients.push(tx); + + Sse::from_infallible_receiver(rx) + } + + /// Broadcasts `msg` to all clients. + pub async fn broadcast(&self, msg: &str) { + let clients = self.inner.lock().clients.clone(); + + let send_futures = clients + .iter() + .map(|client| client.send(sse::Data::new(msg).into())); + + // try to send to all clients, ignoring failures + // disconnected clients will get swept up by `remove_stale_clients` + let _ = future::join_all(send_futures).await; + } +} diff --git a/src/render_browser/common.rs b/src/render_browser/common.rs new file mode 100644 index 0000000..40d0714 --- /dev/null +++ b/src/render_browser/common.rs @@ -0,0 +1,27 @@ +use std::net::TcpListener; + +#[derive(Debug, Clone)] +pub struct UrlToDisplay { + pub url: String, +} + +impl UrlToDisplay { + pub fn new(port: u16) -> UrlToDisplay { + if let Ok(ip) = local_ip_address::local_ip() { + Self { + url: format!("http://{}:{}", ip, port), + } + } else { + return Self { + url: format!("http://localhost:{}", port), + }; + } + } +} + +pub fn port_is_available(port: u16) -> bool { + match TcpListener::bind(("0.0.0.0", port)) { + Ok(_) => true, + Err(_) => false, + } +} diff --git a/src/render_browser/mod.rs b/src/render_browser/mod.rs new file mode 100644 index 0000000..7110d15 --- /dev/null +++ b/src/render_browser/mod.rs @@ -0,0 +1,6 @@ +pub mod broadcast; +pub mod common; +mod render; +mod server; + +pub use crate::render_browser::render::run; diff --git a/src/render_browser/render.rs b/src/render_browser/render.rs new file mode 100644 index 0000000..03ce9ad --- /dev/null +++ b/src/render_browser/render.rs @@ -0,0 +1,23 @@ +use crate::common::format_version_to_display; +use crate::input::parse_gamestate; +use crate::render_browser::common::UrlToDisplay; +use crate::render_browser::server::launch_server; + +pub fn run(port: u16) { + match parse_gamestate() { + Ok(input) => { + let mut options_passthrough = input.options.clone(); + options_passthrough + .features_with_version + .insert("render-browser".to_string(), format_version_to_display()); + let url_to_display = UrlToDisplay::new(port); + options_passthrough.metadatas.insert( + "render-browser-host".to_string(), + format!("{}", url_to_display.url).to_string(), + ); + println!("{}\r", serde_json::to_string(&options_passthrough).unwrap()); + let _ = launch_server(input.lines, options_passthrough, port); + } + Err(_) => todo!(), + } +} diff --git a/src/render_browser/server.rs b/src/render_browser/server.rs new file mode 100644 index 0000000..40dcbec --- /dev/null +++ b/src/render_browser/server.rs @@ -0,0 +1,58 @@ +use std::sync::Arc; + +use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; +use actix_web_static_files::ResourceFiles; + +use crate::input::{Game, InitOptions}; +use crate::render_browser::broadcast::Broadcaster; + +include!(concat!(env!("OUT_DIR"), "/generated.rs")); + +async fn do_broadcast_task(broadcaster: Arc, lines: Box>) { + for line in lines { + let msg = serde_json::to_string(&line).unwrap(); + println!("{}\r", &msg); + broadcaster.broadcast(&msg).await; + } +} + +#[get("/events")] +async fn event_stream(broadcaster: web::Data) -> impl Responder { + broadcaster.new_client().await +} + +#[get("/init-options")] +async fn get_init_options(init_options: web::Data) -> impl Responder { + HttpResponse::Ok().json(init_options) +} + +#[actix_web::main] +pub async fn launch_server( + lines: Box>, + init_options: InitOptions, + port: u16, +) -> std::io::Result<()> { + let broadcaster = Broadcaster::create(); + let broadcaster_clone = broadcaster.clone(); + let rc_init_options = Arc::new(init_options); + + let server = HttpServer::new(move || { + let generated = generate(); + App::new() + .app_data(web::Data::from(Arc::clone(&broadcaster))) + .app_data(web::Data::from(Arc::clone(&rc_init_options))) + .service(event_stream) + .service(get_init_options) + .service(ResourceFiles::new("/", generated)) + }) + .bind(("0.0.0.0", port))? + .run(); + + let server_task = actix_web::rt::spawn(server); + + let broadcast_task = actix_web::rt::spawn(do_broadcast_task(broadcaster_clone, lines)); + + let _ = tokio::try_join!(server_task, broadcast_task).expect("Unable to join tasks"); + + Ok(()) +} diff --git a/src/stream_sse/mod.rs b/src/stream_sse/mod.rs new file mode 100644 index 0000000..092121b --- /dev/null +++ b/src/stream_sse/mod.rs @@ -0,0 +1,4 @@ +mod net; +mod stream; + +pub use crate::stream_sse::stream::run; diff --git a/src/stream_sse/net.rs b/src/stream_sse/net.rs new file mode 100644 index 0000000..1924525 --- /dev/null +++ b/src/stream_sse/net.rs @@ -0,0 +1,42 @@ +use futures_util::StreamExt; +use reqwest::get; +use reqwest_eventsource::{Event, EventSource}; +use serde_json; + +use crate::input::{Game, InitOptions}; + +async fn fetch_init_options(address: &String) -> Result> { + let response = get(format!("{}/init-options", address)).await?; + let init_options = response.json::().await?; + return Ok(init_options); +} + +pub async fn bootstrap(address: String) { + let mut events = EventSource::get(format!("{}/events", address)); + let mut current_init_options: Option = None; + while let Some(event) = events.next().await { + match event { + // what if the sse re-opens with different init_options ? we can't support for the moment - message parsers should ignore the header line + Ok(Event::Open) => { + if let Ok(init_options) = fetch_init_options(&address).await { + println!("{}", serde_json::to_string(&init_options).unwrap()); + current_init_options = Some(init_options); + } + } + Ok(Event::Message(message)) => { + if current_init_options.is_some() { + if let Ok(game_state) = serde_json::from_str::(&message.data) { + println!("{}", serde_json::to_string(&game_state).unwrap()); + } + } + } + Err(_) => { + // for the moment, keep the console clean + // eprintln!( + // "Cannot connect to {}. Please check the server is up. Retrying ...", + // address + // ) + } + } + } +} diff --git a/src/stream_sse/stream.rs b/src/stream_sse/stream.rs new file mode 100644 index 0000000..3d39bf6 --- /dev/null +++ b/src/stream_sse/stream.rs @@ -0,0 +1,8 @@ +use tokio::runtime::Runtime; + +use crate::stream_sse::net::bootstrap; + +pub fn run(address: String) { + let rt = Runtime::new().unwrap(); + rt.block_on(bootstrap(address)); +} diff --git a/src/throttle.rs b/src/throttle.rs new file mode 100644 index 0000000..d4bbe78 --- /dev/null +++ b/src/throttle.rs @@ -0,0 +1,61 @@ +use std::time::{Duration, Instant}; + +use crate::common::format_version_to_display; +use crate::input::{parse_gamestate, Game}; + +const FRAME_ACCURACY: Duration = Duration::from_millis(20); + +pub fn run(frame_duration: u32, loop_infinite: bool) { + let frame_duration_millis = Duration::from_millis(frame_duration as u64); + let mut recording_buffer: Vec = Vec::new(); + match parse_gamestate() { + Ok(mut input) => { + let mut options_passthrough = input.options.clone(); + options_passthrough.frame_duration = frame_duration; + options_passthrough + .features_with_version + .insert("throttle".to_string(), format_version_to_display()); + options_passthrough + .metadatas + .insert("throttled".to_string(), "on".to_string()); + println!("{}\r", serde_json::to_string(&options_passthrough).unwrap()); + let mut last_loop_duration: Duration = Duration::new(0, 0); + let mut replaying_index = 0; + loop { + let start = Instant::now(); + while start.elapsed() < FRAME_ACCURACY { + std::hint::spin_loop(); + } + if last_loop_duration > frame_duration_millis { + if let Some(parsed_line) = input.lines.next() { + recording_buffer.push(parsed_line.clone()); + println!("{}\r", serde_json::to_string(&parsed_line).unwrap()); + } else { + if !loop_infinite { + std::process::exit(0); + } + replaying_index = if replaying_index < recording_buffer.len() { + replaying_index + } else { + 0 + }; + println!( + "{}\r", + serde_json::to_string(recording_buffer.get(replaying_index).unwrap()) + .unwrap() + ); + replaying_index = replaying_index + 1; + } + // adjust framerate + let remainder = last_loop_duration - frame_duration_millis; + last_loop_duration = remainder; + } + last_loop_duration += start.elapsed(); + } + } + Err(e) => { + eprintln!("Error occurred while parsing stdin: \"{}\"", e); + std::process::exit(1); + } + } +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..18d4254 --- /dev/null +++ b/static/index.html @@ -0,0 +1,39 @@ + + + + + + snakepipe render-browser + + + + + + + +
+

render-browser

+

This mirrors your game in the terminal by launching a server, reading the state from stdin, streaming this state through server-sent events, then a JS renderer reads it in the browser and renders it.

+
+

Choose your renderer:

+
+
    +
  • + + +
  • +
  • + + +
  • +
+
+
+
+
+
+ +

Snap the qrcode to watch it on your mobile

+
+ + diff --git a/static/libs/qrcode-display.js b/static/libs/qrcode-display.js new file mode 100644 index 0000000..39a4263 --- /dev/null +++ b/static/libs/qrcode-display.js @@ -0,0 +1,84 @@ +/** + * Retrieved from a previous project of mine: + * https://github.com/topheman/webrtc-remote-control/blob/master/demo/shared/js/components/qrcode-display.js + */ + +if (typeof QRCode === "undefined") { + throw new Error( + "Missing `QRCode` function, please include `qrcode.min.js` as script tags before from https://unpkg.com/qrcodejs@1.0.0/qrcode.min.js" + ); +} + +class QRCodeDisplay extends HTMLElement { + constructor() { + super(); + const shadow = this.attachShadow({ mode: "open" }); + const style = document.createElement("style"); + const run = document.createElement("div"); // wraps the qrcode that will be shown + run.className = "run"; + const build = document.createElement("div"); // wraps the div where the qrcode is built + build.className = "build"; + style.textContent = ` +.build { + display: none; +} + `; + shadow.appendChild(style); + shadow.appendChild(run); + shadow.appendChild(build); + this.render(); + } + + static get observedAttributes() { + return ["data", "width", "height", "wrap-anchor"]; + } + + attributeChangedCallback(attrName, oldVal, newVal) { + if (oldVal !== newVal) { + this.render(); + } + } + + render() { + const data = this.getAttribute("data"); + let wrapAnchor = false; + try { + wrapAnchor = JSON.parse(this.getAttribute("wrap-anchor")); + } catch (e) { + // eslint-disable-next-line no-console + console.warn( + "Wrong `wrap-anchor` attribute passed to `qrcode-display` (only accepts `true` or `false`)" + ); + wrapAnchor = false; + } + if (data) { + this.shadowRoot.querySelector(".build").innerHTML = ""; + /* eslint-disable */ + new QRCode(this.shadowRoot.querySelector(".build"), { + text: this.getAttribute("data"), + width: parseInt(this.getAttribute("width")) || 200, + height: parseInt(this.getAttribute("height")) || 200, + colorDark: "#900000", + }); + /* eslint-enable */ + const img = this.shadowRoot.querySelector(".build img"); + // 😢 + setTimeout(() => { + img.style.display = "initial"; + }, 0); + img.title = data; + this.shadowRoot.querySelector(".run").innerHTML = ""; + if (wrapAnchor) { + const a = document.createElement("a"); + a.href = data; + a.title = data; + a.appendChild(img); + this.shadowRoot.querySelector(".run").appendChild(a); + } else { + this.shadowRoot.querySelector(".run").appendChild(img); + } + } + } +} + +customElements.define("qrcode-display", QRCodeDisplay); diff --git a/static/libs/qrcode.min.js b/static/libs/qrcode.min.js new file mode 100644 index 0000000..993e88f --- /dev/null +++ b/static/libs/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/static/main.css b/static/main.css new file mode 100644 index 0000000..3a0c87a --- /dev/null +++ b/static/main.css @@ -0,0 +1,88 @@ +h1,h2,h3,h4,h5,h6,a { + color: #900000; +} +h1,p,ul { + margin-top: 8px; + margin-bottom: 8px; +} +body { + font-family: Arial,Helvetica,sans-serif; +} +.renderer-switcher-wrapper { + display: flex; +} +#renderer-switcher ul { + display: flex; + padding-left: 0; +} +#renderer-switcher li { + list-style: none; +} +#basic pre { + border: 1px solid #900000; + font-family: 'Courier New', Courier, monospace; + line-height: 16px; +} +#basic-infos { + list-style: none; + padding-left: 0; +} +#inspect-infos { + list-style: none; + padding-left: 0; +} +.layout-qrcode { + max-width: 160px; + font-size: 14px; +} +@media (min-width: 1025px) { + .layout-qrcode { + position: absolute; + top: 0; + right: 0; + margin: 8px; + } + .layout-top { + margin-right: 180px; + } +} +@media (max-width: 1024px) { + .layout-qrcode { + display: block; + margin: 20px auto; + } +} +/** style for basic renderer */ +#basic { + display: flex; + flex-direction: column; + align-items: center; +} +#basic #basic-infos { + text-align: center; +} +#basic #basic-game { + font-size: var(--basic-zoom-font-size); + line-height: var(--basic-zoom-font-size); +} +#basic #zoom-slider-wrapper { + -webkit-user-select: none; /* Safari */ + user-select: none; /* Standard syntax */ +} +#basic #zoom-slider { + margin-top: 20px; + margin-bottom: 20px; + touch-action: manipulation; + user-select: none; +} +#basic [name=increment], #basic [name=decrement] { + width: 48px; + height: 48px; + touch-action: manipulation; /** prevent double-tap from zooming on mobiles */ +} +@media (min-width: 1024px) { + #basic [name=increment], #basic [name=decrement] { + width: 32px; + height: 32px; + } +} diff --git a/static/main.js b/static/main.js new file mode 100644 index 0000000..cf06fa9 --- /dev/null +++ b/static/main.js @@ -0,0 +1,189 @@ +/** + * + * @param {string} label + * @param {HTMLElement} rootNode + */ +function createNodeInsideRootNode(label, rootNode) { + const loadingNode = document.createElement('p'); + loadingNode.innerHTML = label; + rootNode.appendChild(loadingNode); + return null; +} + +/** + * + * @param {"loading" | "connected" | "ready" | "error"} state + * @param {HTMLElement} rootNode + * @param {String?} rendererName + * @returns {HTMLElement | null} + */ +function prepareRootNode(state, rootNode, rendererName) { + switch (state) { + case "loading": { + return createNodeInsideRootNode("Loading ...", rootNode); + } + case "connected": { + return createNodeInsideRootNode("Connecting ...", rootNode); + } + case "error": { + return createNodeInsideRootNode("An error occured, please reload", rootNode); + } + case "ready": { + rootNode.replaceChildren(); + const gameNode = document.createElement('div'); + gameNode.id = rendererName + rootNode.appendChild(gameNode); + return gameNode; + } + } +} + +async function fetchInitOptions() { + try { + const res = await fetch('/init-options'); + if (res.ok) { + return await res.json(); + } + } + catch (e) { + console.error(e); + return null; + } + return null; +} + +/** + * + * @param {String} renderBrowserHost + */ +function getQrcodeUrlToDisplay(renderBrowserHost) { + // if user accessed via ip v4 and renderBrowserHost is ip v4 or user accessed via localhost + if (location.origin === renderBrowserHost || location.hostname === 'localhost') { + return renderBrowserHost; + } + // if user accessed via a hostname resolved outside + return location.origin; +} + +/** + * + * @param {(eventName: 'connected' | 'event', payload: any) => void} cb + */ +async function bootstrap(cb) { + const events = new EventSource("/events"); + events.onmessage = (event) => { + if (event.data === 'connected') { + fetchInitOptions().then(initOptions => { + if (initOptions.metadatas['render-browser-host']) { + document.querySelector('qrcode-display').setAttribute('data', getQrcodeUrlToDisplay(initOptions.metadatas['render-browser-host'])); + } + cb('connected', initOptions); + }) + } + else { + cb('event', JSON.parse(event.data)); + } + } +} + +/** + * + * @type {Record} + */ +const renderers = {}; + +/** + * + * @returns {string} + */ +function getRendererName() { + const rendererSwitcher = document.getElementById('renderer-switcher'); + const rendererName = rendererSwitcher.rendererName.value; + return rendererName +} + +/** + * + * @returns {import("./types").Renderer} + */ +async function getRenderer() { + const rendererName = getRendererName(); + if (renderers[rendererName]) { + return renderers[rendererName]; + } + renderers[rendererName] = await import(`/renderers/${rendererName}.js`); + return renderers[rendererName]; +} + +/** + * @typedef {import("./types").Renderer} Renderer + * @param {(renderer: Renderer) => {}} cb + */ +function onUpdateRender(cb) { + [...document.querySelectorAll('[name=rendererName]')].forEach(node => { + node.addEventListener('change', async () => { + cb(await getRenderer()); + }) + }) +} + +/** + * Main function + */ +async function prepareGame() { + const rootNode = document.getElementById('root'); + prepareRootNode("loading", rootNode); + /** @type {HTMLElement} */ + let gameNode = null; + /** @type {import("./types").Renderer | null} */ + let currentRenderer = null; + let currentRendererContext = null; + let currentCleanupFunction = null; + /** @type {import("./types").InitOptions | null} */ + let currentInitOptions = null; + + /** + * + * @param {import("./types").Renderer} renderer + */ + function updateRenderer(renderer) { + if (currentCleanupFunction) { + currentCleanupFunction(); + } + currentRenderer = renderer; + if (currentInitOptions) { + gameNode = prepareRootNode('ready', rootNode, getRendererName()); + const { cleanup, context } = currentRenderer.setup(currentInitOptions, gameNode); + currentRendererContext = context; + currentCleanupFunction = cleanup + } + + } + currentRenderer = await getRenderer(); + updateRenderer(currentRenderer); + onUpdateRender(updateRenderer); + + function process(eventName, payload) { + switch (eventName) { + case 'connected': + console.log("connected", payload); + currentInitOptions = payload; + gameNode = prepareRootNode('ready', rootNode, getRendererName()); + const { cleanup, context } = currentRenderer.setup(currentInitOptions, gameNode); + currentRendererContext = context; + currentCleanupFunction = cleanup + break; + case 'event': + console.log("event", payload); + if (currentInitOptions) { + currentRenderer.renderFrame(currentInitOptions, payload, currentRendererContext); + } + break + default: + console.error(`Unsupported "${eventName}" event`); + } + } + bootstrap(process); +} + +prepareGame(); diff --git a/static/renderers/basic.js b/static/renderers/basic.js new file mode 100644 index 0000000..8275a0f --- /dev/null +++ b/static/renderers/basic.js @@ -0,0 +1,90 @@ +import { makeRenderInfos } from './utils.js' + +const renderInfos = makeRenderInfos(["score", "version"]); + +/** + * Basic render function + * + * @type {import("../types").Renderer["renderFrame"]} + */ +export function renderFrame(initOptions, frameInfos, context) { + const buffer = []; + for (let i = 0; i < initOptions.size.height; i++) { + buffer.push(Array.from({ length: initOptions.size.width }, () => '·')); + } + buffer[frameInfos.snake.head.y][frameInfos.snake.head.x] = 'H'; + buffer[frameInfos.fruit.y][frameInfos.fruit.x] = 'F'; + frameInfos.snake.tail.forEach(tailFragment => { + buffer[tailFragment.y][tailFragment.x] = 'T'; + }); + const rendered = buffer.map(row => `${row.join('')}`).join('\r\n'); + context.preNode.textContent = rendered; + renderInfos(initOptions, frameInfos, context.infosNode); +} + +/** + * + * @param {HTMLElement} rootNode + */ +function prepareZoomSlider(rootNode, zoomLevel = { min: 5, max: 24, defaultValue: 16 }) { + function updateCssZoomValue(value, zoomLevel) { + if (value >= zoomLevel.min && value <= zoomLevel.max) { + rootNode.style.setProperty('--basic-zoom-value', value); + rootNode.style.setProperty('--basic-zoom-font-size', `${value}px`); + return true; + } + return false + } + updateCssZoomValue(zoomLevel.defaultValue, zoomLevel); + const zoomSliderWrapper = document.createElement('div'); + zoomSliderWrapper.id = "zoom-slider-wrapper"; + zoomSliderWrapper.innerHTML = ` + + + + `; + rootNode.appendChild(zoomSliderWrapper); + const zoomSlider = document.getElementById('zoom-slider'); + zoomSlider.value = rootNode.style.getPropertyValue('--basic-zoom-value'); + zoomSlider.addEventListener('input', function (event) { + updateCssZoomValue(event.target.value, zoomLevel); + }); + zoomSliderWrapper.addEventListener('click', (event) => { + let currentValue = Number(rootNode.style.getPropertyValue('--basic-zoom-value')); + if (event.target.matches('[name=increment]')) { + const newValue = currentValue + 1; + if (updateCssZoomValue(newValue, zoomLevel)) { + zoomSlider.value = newValue; + } + } + if (event.target.matches('[name=decrement]')) { + const newValue = currentValue - 1; + if (updateCssZoomValue(newValue, zoomLevel)) { + zoomSlider.value = newValue; + } + } + }); +} + +/** + * + * @type {import("../types").Renderer["setup"]} + */ +export function setup(initOptions, rootNode) { + prepareZoomSlider(rootNode); + const preNode = document.createElement('pre'); + preNode.id = "basic-game"; + rootNode.appendChild(preNode); + const infosNode = document.createElement('ul'); + infosNode.id = "basic-infos"; + rootNode.appendChild(infosNode); + preNode.style.width = `${initOptions.size.width}ch`; + preNode.style.height = `calc(${preNode.style.lineHeight}*${initOptions.size.height})`; + return { + context: { + preNode, + infosNode, + }, + cleanup: () => { } + }; +} diff --git a/static/renderers/inspect.js b/static/renderers/inspect.js new file mode 100644 index 0000000..9dabf77 --- /dev/null +++ b/static/renderers/inspect.js @@ -0,0 +1,40 @@ +import { makeRenderInfos } from './utils.js' + +const renderInfos = makeRenderInfos(["version"]); + +/** + * Basic render function + * + * @type {import("../types").Renderer["renderFrame"]} + */ +export function renderFrame(initOptions, frameInfos, context) { + context.gameNode.innerHTML = ` +
  • State: ${frameInfos.state}
  • +
  • Score: ${frameInfos.score}
  • +
  • Fruit: x: ${frameInfos.fruit.x} / y: ${frameInfos.fruit.y}
  • +
  • Snake Head: x: ${frameInfos.snake.head.x} / y: ${frameInfos.snake.head.y}
  • +
  • Snake Tail:
      ${frameInfos.snake.tail.map(item => { + return `
    • x: ${item.x} / y: ${item.y}
    • ` + }).join('')}
  • ` + renderInfos(initOptions, frameInfos, context.infosNode); +} + +/** + * + * @type {import("../types").Renderer["setup"]} + */ +export function setup(initOptions, rootNode) { + const gameNode = document.createElement('ul'); + gameNode.id = "inspect-game"; + rootNode.appendChild(gameNode); + const infosNode = document.createElement('ul'); + infosNode.id = "basic-infos"; + rootNode.appendChild(infosNode); + return { + context: { + gameNode, + infosNode, + }, + cleanup: () => { } + }; +} diff --git a/static/renderers/utils.js b/static/renderers/utils.js new file mode 100644 index 0000000..dbadbef --- /dev/null +++ b/static/renderers/utils.js @@ -0,0 +1,42 @@ +/** + * + * @param {Array<"score" | "version">} features + * @returns + */ +export function makeRenderInfos(features) { + /** + * + * @param {import("../types").InitOptions} initOptions + * @param {import("../types").Game} frameInfos + * @param {HTMLElement} infosNode + */ + return function renderInfos(initOptions, frameInfos, infosNode) { + const infos = [ + features.includes("score") ? `
  • Score: ${frameInfos.score} - ${frameInfos.state}
  • ` : false, + features.includes("version") ? `
  • ${makeVersion(initOptions.featuresWithVersion)}
  • ` : false, + ].filter(Boolean); + infosNode.innerHTML = infos.join(''); + } +} + +/** + * Same implementation as in `src/common.rs` + * @param {Record} featuresWithVersion + */ +export function makeVersion(featuresWithVersion) { + const versionsWithFeatures = Object.entries(featuresWithVersion).reduce((acc, [feature, version]) => { + let tmp; + if (acc[version]) { + acc[version].push(feature); + } + else { + acc[version] = [feature]; + } + return acc; + }, {}); + console.log("versionsWithFeatures", versionsWithFeatures, versionsWithFeatures.size); + if (Object.keys(versionsWithFeatures).length === 1) { + return Object.keys(versionsWithFeatures)[0]; + } + return Object.entries(versionsWithFeatures).map(([version, features]) => `${version}: ${features.join('/')}`).join(' - '); +} diff --git a/static/types.d.ts b/static/types.d.ts new file mode 100644 index 0000000..1f6ce9b --- /dev/null +++ b/static/types.d.ts @@ -0,0 +1,48 @@ +export type Size = { + width: number + height: number +} + +export type GameState = "paused" | "over" | "running"; + +export type Game = { + snake: { + direction: string + head: { + x: number + y: number + } + tail: { + x: number + y: number + }[] + } + fruit: { + x: number + y: number + } + score: number + state: GameState +} + +export type InitOptions = { + frameDuration: number + size: { + width: number + height: number + } + featuresWithVersion: Record + metadatas: Record +} + +export type SetupFunction = (initOptions: InitOptions, gameNode: HTMLElement) => { + context: any, + cleanup: () => {} +} + +export type RenderFrameFunction = (size: InitOptions, frameInfos: Game, context: any) => {} + +export type Renderer = { + setup: SetupFunction, + renderFrame: RenderFrameFunction +}